﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Axiom.Math;
using Axiom;

namespace w4nted
{
    class Netzwerk
    {
        #region Variables and Declarations
        internal List<player> playerliste = new List<player>();
        internal player me = new player(new IPEndPoint(Dns.GetHostAddresses(Dns.GetHostName())[0], Port));
        internal Thread thread_listen;
        internal Thread tread_send;
        internal Thread thread_broadcast_send;
        private const int Port = 11000;
        private Boolean net_up = false;
        UdpClient listener = new UdpClient(Port);
        IPEndPoint listenerEP = new IPEndPoint(IPAddress.Any, Port);

        private MemoryStream mem_broadcast_listener = new MemoryStream();
        private BinaryFormatter binary = new BinaryFormatter();
        private IPAddress myip = Dns.GetHostAddresses(Dns.GetHostName())[0];
        internal Queue<NetShoot> Shoots_to_send = new Queue<NetShoot>();
        internal Queue<NetShoot> Shoots_recieved = new Queue<NetShoot>();
        #endregion

        internal void StartNetwork()
        {
            net_up = true;

            thread_listen = new Thread(new ThreadStart(Listener));
            thread_listen.Start();

            tread_send = new Thread(new ThreadStart(Sender));
            tread_send.Start();

            thread_broadcast_send = new Thread(new ThreadStart(Broadcast));
            thread_broadcast_send.Start();
        }

        internal void StopNetwork()
        {
            net_up = false;

            thread_listen.Abort() ;

            tread_send.Abort() ;

            thread_broadcast_send.Abort();
        }

        #region Broadcast
        //Starts the timer who send broadcasts all 2000 ms in the local network, to make it visible for other players
        internal void Broadcast()
        {
            System.Timers.Timer timer = new System.Timers.Timer();
            timer.Elapsed += new System.Timers.ElapsedEventHandler(Broadcast_send);
            timer.Interval = 2000;
            timer.Start();

            while (net_up)
            {
                System.Threading.Thread.Sleep(1000);
            }
            timer.Stop();
        }

        // Sends the Broadcast-message with the ip to connect to
        internal void Broadcast_send(object source, System.Timers.ElapsedEventArgs e)
        {
            IPEndPoint broadcast = new IPEndPoint(IPAddress.Broadcast, Port);
            System.Timers.Timer timer = new System.Timers.Timer();

            Sendbuf buffer = new Sendbuf();
            buffer.type = Sendbuf.PackageType.Broadcast;
            buffer.AddObject(myip);

            Socket ss = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            ss.EnableBroadcast = true;
            ss.SendTo(buffer.CompressedSendBuffer(), broadcast);
            ss.Close();
        }
        #endregion

        internal void AddShoot(Vector3 v, Quaternion q)
        {
            NetShoot tmp_shoot = new NetShoot(v, q);
            tmp_shoot.ip_address = me.host_ep.Address.GetAddressBytes();
            Shoots_to_send.Enqueue(tmp_shoot);
        }

        [Serializable()]
        internal class NetShoot
        {
            internal float x, y, z, qx, qy, qz, qw;
            internal byte[] ip_address;
            public NetShoot(Vector3 v, Quaternion q)
            {
                x = v.x;
                y = v.y;
                z = v.z;
                qx = q.x;
                qy = q.y;
                qz = q.z;
                qw = q.w;
            }

            public NetShoot(NetShoot s)
            {
                x = s.x;
                y = s.y;
                z = s.z;
                qx = s.qx;
                qy = s.qy;
                qz = s.qz;
                qw = s.qw;
                ip_address = s.ip_address;
            }
        }

        //Recieve all Packages sent to this pc on the specivied port
        internal void Listener()
        {
            while (net_up)
            {
                byte[] bytes = listener.Receive(ref listenerEP);
                try
                {
                    Sendbuf buffer = new Sendbuf();
                    buffer.ReadCompressedBuffer(bytes);

                    //Switch on the type to do the right action on the data
                    switch (buffer.type)
                    {
                        #region Shoot_update
                        case Sendbuf.PackageType.Shoot:
                            MemoryStream mem_shoot = new MemoryStream(buffer.buffer);
                            NetShoot shoot = (NetShoot)binary.Deserialize(mem_shoot);
                            Shoots_recieved.Enqueue(new NetShoot(shoot));
                            mem_shoot.Close();
                            break;
                        #endregion

                        #region Broadcast
                        case Sendbuf.PackageType.Broadcast:
                            MemoryStream mem_broadcast_listener = new MemoryStream(buffer.buffer);
                            IPAddress p = (IPAddress)binary.Deserialize(mem_broadcast_listener);
                            if (!myip.Equals(p))
                            {
                                Boolean is_player_in_list = false;
                                foreach (player check_broadcast_player in playerliste)
                                {
                                    if (p.Equals(check_broadcast_player.host_ep.Address)) 
                                    {
                                        is_player_in_list = true;
                                    }
                                }
                                if (!is_player_in_list)
                                {
                                    playerliste.Add(new player(new IPEndPoint(p,Port)));
                                    System.Console.WriteLine("Added player with ip" + p.ToString());
                                }
                            }
                            mem_broadcast_listener.Close();
                            break;
                        #endregion                       
                            
                        #region Player_update
                        case Sendbuf.PackageType.Player_update:
                            MemoryStream mem2 = new MemoryStream(buffer.buffer);
                            player player = (player)binary.Deserialize(mem2);
                            foreach (player pl in playerliste)
                            {
                                if (pl.host_ep.Address.Equals(player.host_ep.Address))
                                {
                                    pl.position = player.position;
                                    pl.orientation = player.orientation;
                                    pl.name = player.name;
                                    pl.hp = player.hp;
                                }
                            }
                            mem2.Close();
                            break;
                        #endregion
                    }
                }
                catch{}
                finally{}
                System.Threading.Thread.Sleep(1);
            }
        }
        
        //Start the Sender-Thread, who sends the information of this player to all other players in the list
        internal void Sender()
        {
            Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); //open Sending Socket
            while (net_up)
            {
                System.Threading.Thread.Sleep(1000); // Sleep if no other players are in the List
                while (playerliste.Count > 0)
                {
                    if (Shoots_to_send.Count > 0 )
                    {
                        Sendbuf buf = new Sendbuf(); // New Sendbuf 
                        buf.type = Sendbuf.PackageType.Shoot; //Set Type of sendbuf
                        buf.AddObject(Shoots_to_send.Dequeue());
                        byte[] toSend = buf.CompressedSendBuffer(); //Get compressed Array of Bytes to send over the net
                        foreach (player p in playerliste) //Send Updated information to each player in the Playerliste
                        {
                            s.SendTo(toSend, p.host_ep); //Send buffer (toSend) over the socket to the other player
                        }
                    }
                    else
                    {
                        Sendbuf buf = new Sendbuf(); // New Sendbuf 
                        buf.type = Sendbuf.PackageType.Player_update; //Set Type of sendbuf
                        buf.AddObject(me);  //Add player object (me) to buffer
                        byte[] toSend = buf.CompressedSendBuffer(); //Get compressed Array of Bytes to send over the net

                        try
                        {
                            foreach (player p in playerliste) //Send Updated information to each player in the Playerliste
                            {
                                s.SendTo(toSend, p.host_ep); //Send buffer (toSend) over the socket to the other player
                            }
                        }
                        catch { }
                        finally { }


                        System.Threading.Thread.Sleep(2); //Sleep a short time to avoid network-flooding 
                    }
                }
            }
        }

        internal class Sendbuf
        {
            public byte[] buffer;
            public PackageType type;
            private BinaryFormatter binary = new BinaryFormatter();

            //Add an object to the Buffer (binarize it)
            public void AddObject(Object o)
            {
                MemoryStream mem = new MemoryStream();
                binary.Serialize(mem, o);
                buffer = mem.GetBuffer();
                mem.Close();
            }

            //Returns the compressed byte[] array representing this Sendbuf Object
            public byte[] CompressedSendBuffer()
            {
                byte[] result;
                MemoryStream mem = new MemoryStream(); //Open memory-Stream
                mem.Write(BitConverter.GetBytes(buffer.Length + 1), 0, 4); // Add lenght of Memory-Stream
                using (BinaryWriter comp = new BinaryWriter(new GZipStream(mem, CompressionMode.Compress, true))) //Open Compressor
                {
                    comp.Write((byte)type); //Add packateType
                    comp.Write(buffer); //Add buffer of Data do be sent
                }
                result = mem.ToArray(); //Save Compressed buffer
                mem.Close(); //Close Memory-Stream
                return result;
            }

            //Reads in a compressed byte[] array and set the current Object to it's info
            public void ReadCompressedBuffer(byte[] b)
            {
                int count = BitConverter.ToInt32(b, 0); //Get length of uncompressed Buffer
                MemoryStream mem = new MemoryStream(); //Open memory-Stream
                mem.Write(b, 4, b.Length - 4); //Add data of buffer to memory-stream
                mem.Position = 0;   //Set Memory-Stream Position to the beginning
                using (BinaryReader decomp = new BinaryReader(new GZipStream(mem, CompressionMode.Decompress))) //Open DeCompressor
                {
                    type = (PackageType)decomp.ReadByte(); //Read Packagetype
                    buffer = decomp.ReadBytes(count - 1); //Read Data into buffer 
                }
                mem.Close(); //Close Memory-Stream
            }

            //Check the type of the current sendbuf Object
            public bool IsOfType(PackageType t)
            {
                return (int)type == (int)t;
            }

            //An enummeration of all posssible PackageTypes for Sendbuf
            public enum PackageType : byte
            {
                Broadcast = 1,
                Player_update = 2,
                Shoot = 3
            }

        }
        
        #region Compressions Methods (Out of Date)
        /*
        public byte[] Compress(byte[] b)
        {
            MemoryStream ms = new MemoryStream();
            ms.Write(BitConverter.GetBytes(b.Length), 0, 4);
            using (BinaryWriter comp = new BinaryWriter(new GZipStream(ms, CompressionMode.Compress, true)))
            {
                comp.Write(b);
            }
            return ms.ToArray();
        }

        internal byte[] Decompress(byte[] b)
        {
            byte[] result;
            int count = BitConverter.ToInt32(b, 0);
            MemoryStream ms = new MemoryStream();
            ms.Write(b, 4, b.Length - 4);
            ms.Position = 0;
            using (BinaryReader decomp = new BinaryReader(new GZipStream(ms, CompressionMode.Decompress)))
            {
                result = decomp.ReadBytes(count);
            }
            return result;
        */
        #endregion
    }
}
